use core::f32;
use std::{
    time::{Duration, Instant}, ptr,
    thread, sync::{LazyLock, OnceLock, atomic::*},
};
use rand;
use crate::{
    config::{self, Config}, defines::*, game::*, learn::WholeFrame, log, utils::lock_free_dt::IndexedList,
};


const N_PIECES_IN_MID:usize = 13;
const N_PIECES_IN_SEARCH:usize = 65;
const N_PIECES_IN_ENDING:usize = 85;
const SAY_HAS_WIN:&str = "(●'◡'●)";
const SAY_MAYBE_DRAW:&str = "(★ ω ★)";
const SAY_WILL_LOSE:&str = "(T_T)";
const SAY_WHAT:&str = "ಠ_ಠ";

const LEVEL_SEARCH_MAP:[usize;10] = [
    6, 7, 8, 9, 10, 11, 12, 13, 14, 15
];

const UCB_C:f32 = 1.6487212707001281;                   // C = (sqrt e)
const UCB_C_END:f32 = 1.2;
const UCB_C_DW:f32 = UCB_C - UCB_C_END;

const UCT_SIMULATE_DRAW:f32 = 0.2;
const UCT_SA_LOC_EDGE:[u8;BOARD_LEN] = [
    0, 0, 1, 1, 1, 1, 1, 1, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    1, 0, 0, 0, 0, 0, 0, 0, 0, 1,
    1, 0, 0, 0, 0, 0, 0, 0, 0, 1,
    1, 0, 0, 0, 0, 0, 0, 0, 0, 1,
    1, 0, 0, 0, 0, 0, 0, 0, 0, 1,
    1, 0, 0, 0, 0, 0, 0, 0, 0, 1,
    1, 0, 0, 0, 0, 0, 0, 0, 0, 1,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 1, 1, 1, 1, 1, 1, 0, 0,
];
const UCT_SA_LOC_V:[u8;BOARD_LEN] = [
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 1, 0, 0, 0, 0, 1, 0, 0,
    0, 1, 1, 0, 0, 0, 0, 1, 1, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 1, 1, 0, 0, 0, 0, 1, 1, 0,
    0, 0, 1, 0, 0, 0, 0, 1, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
];
const UCT_SA_LOC_C:[u8;BOARD_LEN] = [
    0, 1, 0, 0, 0, 0, 0, 0, 1, 0,
    1, 1, 0, 0, 0, 0, 0, 0, 1, 1,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    1, 1, 0, 0, 0, 0, 0, 0, 1, 1,
    0, 1, 0, 0, 0, 0, 0, 0, 1, 0,
];
const UCT_LTS:[&[u8;BOARD_LEN];4] = [&LOC_IN_CORNER, &UCT_SA_LOC_EDGE, &UCT_SA_LOC_C, &UCT_SA_LOC_V];
const LOCS_BEGIN_RANGOM:[u8;BOARD_LEN] = [
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 1, 1, 1, 1, 0, 0, 0,
    0, 0, 1, 1, 1, 1, 1, 1, 0, 0,
    0, 0, 1, 1, 1, 1, 1, 1, 0, 0,
    0, 0, 1, 1, 1, 1, 1, 1, 0, 0,
    0, 0, 1, 1, 1, 1, 1, 1, 0, 0,
    0, 0, 0, 1, 1, 1, 1, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
    0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
];

pub trait YieldFns {
    fn report(&self, text:String);
    fn say(&self, text:String);
    fn debug(&self, text:String);
}

#[derive(Debug, Default, Clone)]
struct MovesCI {
    bits_piece: u128,
    bits_color: u128,
    bits_all: [u128;2],
}
const MOVES_CACHE_CAPACITY:usize = MOVES_CACHE_SIZE_MAX / size_of::<MovesCI>();
struct MovesCacheTable {
    items: IndexedList<MovesCI>,
}
impl MovesCacheTable {
    fn new() -> Self {
        Self { items: IndexedList::new(MOVES_CACHE_CAPACITY) }
    }
    fn capacity(&self) -> usize {self.items.capacity()}
    fn get(&self, board:&Board) -> Option<MovesCI> {
        let hash = board.hash();
        let i = hash as usize % MOVES_CACHE_CAPACITY;
        if let Some(item) = self.items.get_ref(i) {
            if (board.bits_piece == item.bits_piece) && (board.bits_color == item.bits_color) {
                Some(item.clone())
            } else {
                None
            }
        } else {
            None
        }
    }
    fn set(&self, board:&Board, bits_all:[u128;2]) {
        let i = board.hash() as usize % MOVES_CACHE_CAPACITY;
        let _ = self.items.set(i, MovesCI {
            bits_piece: board.bits_piece,
            bits_color: board.bits_color,
            bits_all,
        });
    }
}
struct Shared {
    background_running: AtomicBool,
    background_handle: IndexedList<thread::JoinHandle<()>>,
    last_board: IndexedList<Board>,
    moves_fn: OnceLock<Box<dyn Fn(&Board, Piece)->Vec<Loc>>>,
    moves_cache_table: OnceLock<MovesCacheTable>,
    whole_locs: IndexedList<[f32; BOARD_LEN]>,
}
impl Shared {
    #[inline] fn moves(&self, board:&Board, pcur:Piece) -> Vec<Loc> {
        let f = self.moves_fn.get().unwrap();
        f(board, pcur)
    }
    #[inline] fn moves_cache_table(&self) -> &MovesCacheTable {
        self.moves_cache_table.get().unwrap()
    }
    #[inline] fn lws(&self) -> [&'static [f32;BOARD_LEN]; 2] {
        [SHARED.whole_locs.get_ref(0).unwrap(), SHARED.whole_locs.get_ref(1).unwrap()]
    }
}
unsafe impl Sync for Shared {}
unsafe impl Send for Shared {}
static SHARED:LazyLock<Shared> = LazyLock::new(||{
    Shared {
        background_running: AtomicBool::new(false),
        background_handle: IndexedList::new(1),
        last_board: IndexedList::new(1),
        moves_fn: OnceLock::new(),
        moves_cache_table: OnceLock::new(),
        whole_locs: IndexedList::new(2),
    }
});

fn moves_normal(board:&Board, pcur:Piece) -> Vec<Loc> {
    board.moves(pcur)
}
fn moves_cached(board:&Board, pcur:Piece) -> Vec<Loc> {
    let pcur_i = pcur as usize - 1;
    if let Some(ci) = SHARED.moves_cache_table().get(board) {
        loc_bits_to_vec(ci.bits_all[pcur_i])
    } else {
        let bits_all = [board.moves_bits(Piece::Black), board.moves_bits(Piece::White)];
        SHARED.moves_cache_table().set(board, bits_all);
        loc_bits_to_vec(bits_all[pcur_i])
    }
}

fn valuation_n_pieces(n_black:usize, n_white:usize) -> f32 {
    if n_black + n_white >= BOARD_LEN {
        if n_black == n_white {
            0.0
        } else if n_black < n_white {
            1.0
        } else {
            -1.0
        }
    } else {
        let n_diff = (n_white as f32) - (n_black as f32);
        let diff_way = (2.0 * (n_diff >= 0.0) as i32 as f32) - 1.0;
        (1.0 - (1.0 / (1.0 + n_diff.abs()))) * diff_way
    }
}
fn valuation_whole(board:&Board, pcur:Piece, lw:&[f32;BOARD_LEN], lw_sum:f32) -> f32 {
    let mut score = 0f32;
    for i in 0..BOARD_LEN {
        let p = board.get(i as Loc);
        score += p.way() * lw[i];
    }
    (score / lw_sum) * pcur.way()
}
fn valuation_being(board:&Board) -> f32 {
    let n_black = board.n_black() as f32;
    let n_black_actives = board.moves(Piece::Black).len() as f32;
    let score_black = (1.0 - (n_black / (BOARD_LEN as f32))) * n_black_actives;
    let n_white = board.n_white() as f32;
    let n_white_actives = board.moves(Piece::White).len() as f32;
    let score_white = (1.0 - (n_white / (BOARD_LEN as f32))) * n_white_actives;
    score_white - score_black
}

struct Node {
    parent: *mut Node,
    pcur: Piece,
    loc: Loc,
    n_select: u32,
    n_wins: f32,
    n_whole: f32,
    puct_p: f32,
    nexts: Vec<Node>,
    next_moves: Vec<Loc>,
    next_puct_ps: Vec<f32>,
    has_over: bool,
}

impl Node {
    fn new<WF: Fn(&Board,Piece)->f32>(parent:*mut Node, pcur:Piece, board:&Board, loc:Loc, puct_p:f32, wf:&WF) -> Self {
        let n_empty = BOARD_LEN - board.n_piece();
        let next_moves:Vec<Loc> = if n_empty > 0 {
            board.moves(pcur.side())
        } else {
            Vec::new()
        };
        let mut whole_sum = 0.0;
        let next_wholes:Vec<f32> = next_moves.iter().map(|loc|{
            let mut board = board.clone();
            board.down(*loc, pcur.side());
            let whole = wf(&board, pcur.side());
            whole_sum += whole;
            whole
        }).collect();
        let next_puct_ps = next_wholes.iter().map(|a|{
            *a / whole_sum
        }).collect();
        Self {
            parent, pcur, loc,
            n_select: 0, n_wins: 0.0, n_whole: 0.0, puct_p,
            nexts: Vec::new(), next_moves, next_puct_ps, has_over: (n_empty < 1),
        }
    }
    fn expend<WF: Fn(&Board,Piece)->f32>(&mut self, board:&mut Board, wf:&WF) -> &mut Node {
        let side = self.pcur.side();
        let mut loc = loc_invalid();
        let mut puct_p = 0.0;
        if self.next_moves.len() > 0 {
            let hit_i = rand::random_range(0..self.next_moves.len());
            loc = self.next_moves.swap_remove(hit_i);
            puct_p = self.next_puct_ps.swap_remove(hit_i);
            board.down(loc, side);
        }
        let next = Node::new(self, side, board, loc, puct_p, wf);
        self.nexts.push(next);
        self.nexts.last_mut().unwrap()
    }
    #[inline] fn has_over(&self) -> bool {self.has_over}
    #[inline] fn has_fully(&self) -> bool {self.next_moves.len() < 1 && self.nexts.len() > 0}
    #[inline] fn avg_wins(&self) -> f32 {self.n_wins / self.n_select as f32}
    #[inline] fn avg_whole(&self) -> f32 {self.n_whole / self.n_select as f32}
    #[inline] fn puct_p(&self) -> f32 {self.puct_p}
}

fn puct(node:&Node, ucb_c:f32) -> usize {
    let mut hit_i = 0usize;
    let mut max_bonus = f32::MIN;
    let pre_sel = (node.n_select as f32).sqrt();
    for i in 0..node.nexts.len() {
        let next = &node.nexts[i];
        if next.n_select < 1 {
            return i;
        }
        let ucb_c = if next.avg_wins() < UCT_SIMULATE_DRAW {UCB_C} else {ucb_c};
        let ucb_explore = ucb_c * next.puct_p() * (pre_sel / next.n_select as f32);
        let ucb_rewind = next.avg_wins();
        let bonus = ucb_explore + ucb_rewind;
        if max_bonus < bonus {
            max_bonus = bonus;
            hit_i = i;
        }
    }
    hit_i
}

fn uct_select<'a, WF: Fn(&Board,Piece)->f32>(node:&'a mut Node, board:&mut Board, ucb_c:f32, wf:&WF) -> &'a mut Node {
    if node.has_over() {
        node
    } else if node.has_fully() {
        let i = puct(node, ucb_c);
        let next = &mut node.nexts[i];
        if loc_in_board(next.loc) {
            board.down(next.loc, next.pcur);
        }
        uct_select(next, board, ucb_c, wf)
    } else {
        node.expend(board, wf)
    }
}

fn uct_value_unfill(board:&Board) -> f32 {
    let n_black = board.n_black();
    let n_white = board.n_white();
    if n_black > n_white {
        -1.0
    } else if n_white > n_black {
        1.0
    } else {
        UCT_SIMULATE_DRAW
    }
}

fn uct_value(board:&Board) -> Option<f32> {
    let n_black = board.n_black();
    let n_white = board.n_white();
    if n_black + n_white < BOARD_LEN {
        None
    } else if n_black > n_white {
        Some(-1.0)
    } else if n_white > n_black {
        Some(1.0)
    } else {
        Some(UCT_SIMULATE_DRAW)
    }
}

struct SimulateResult {
    end_way: f32,               // -1.0 lose < draw < 1.0 win
    whole: f32,
}
impl Default for SimulateResult {
    fn default() -> Self {
        Self {end_way: 0.0, whole: 0.0}
    }
}
fn uct_simulate<WF: Fn(&Board,Piece)->f32>(time_board:&mut Board, node:&Node, wf:&WF) -> SimulateResult {
    let mut sr = SimulateResult::default();
    let mut n_pass = 0;
    let mut side = node.pcur.side();
    let mut sim_board = time_board.clone();
    let lws = SHARED.lws();
    while n_pass < 1 {
        if let Some(value) = uct_value(&sim_board) {
            sr.end_way = value * node.pcur.way();
            break
        }
        let moves = SHARED.moves(&sim_board, side);
        if moves.len() > 0 {
            let next_loc = uct_rollout(&moves, side, lws[side as usize - 1]);
            sim_board.down(next_loc, side);
            n_pass = 0;
        } else {
            n_pass += 1;
            if n_pass > 1 {
                sr.end_way = uct_value_unfill(&sim_board) * node.pcur.way();
            }
        }
        side = side.side();
    }
    sr.whole = wf(&sim_board, node.pcur);
    sr
}
fn uct_rollout(moves:&[Loc], pcur:Piece, lw:&[f32;BOARD_LEN]) -> Loc {
    let hits:[Vec<Loc>;4] = UCT_LTS.map(|lt|{
        moves.iter().filter_map(|loc|{if lt[*loc as usize] > 0 {Some(*loc)} else {None}}).collect()
    });
    for hit in &hits {
        if ! hit.is_empty() {
            return hit[rand::random_range(0..hit.len())];
        }
    }
    let mut max = (0, lw[moves[0] as usize]);
    for i in 1..moves.len() {
        let v = lw[moves[i] as usize];
        if v > max.1 {
            max = (i, v);
        }
    }
    moves[max.0]
}
fn uct_backup(node:&mut Node, sr:SimulateResult) {
    fn backup(leaf_pcur:Piece, node:*mut Node, sr:&SimulateResult) {
        if ! node.is_null() {
            let node = unsafe { node.as_mut().unwrap() };
            let add_way = (2.0 * ((node.pcur == leaf_pcur) as i32 as f32)) - 1.0;
            let dw = 0.01 + (rand::random_range(0..100) as f32 / 100.0);
            node.n_wins += add_way * sr.end_way * dw;
            node.n_whole += add_way * sr.whole * dw;
            node.n_select += 1;
            backup(leaf_pcur, node.parent, sr)
        }
    }
    backup(node.pcur, node as *mut Node, &sr)
}
fn step_uct<WF: Fn(&Board,Piece)->f32>(pcur:Piece, board:&Board, max_times:usize, level:usize, wf:&WF, fns:&dyn YieldFns) -> Loc {
    const REPORT_DURATION:Duration = Duration::from_millis(ENGINE_REPORT_MS as u64);
    fns.report(format!("UCT {}", max_times));
    let mut root = Node::new(ptr::null_mut(), pcur.side(), board, loc_invalid(), 0.0, wf);
    let phase = ((board.n_piece() as f32 - 4.0) / (N_PIECES_IN_ENDING-4) as f32).max(0.0);
    let ucb_c = UCB_C - (UCB_C_DW * phase);
    fns.debug(format!("sslash-local | phase:{phase:.3} | ucb-c:{ucb_c:.3} | max-times:{max_times} | level:{level}"));
    let times_end = max_times + (phase * max_times as f32) as usize;
    let mut time_last = Instant::now();
    for times in 0..times_end {
        let mut time_board = board.clone();
        let node = uct_select(&mut root, &mut time_board, ucb_c, &wf);
        let mut sr = uct_simulate(&mut time_board, node, &wf);
        uct_backup(node, sr);
        if time_last.elapsed() > REPORT_DURATION {
            time_last = Instant::now();
            fns.report(format!("UCT {}", max_times as i32 - times as i32))
        }
    }
    root.nexts.sort_by(|a,b|{
        let a = a.avg_wins() - (1.0 / a.n_select as f32);
        let b = b.avg_wins() - (1.0 / b.n_select as f32);
        b.total_cmp(&a)
    });
    let mut debug_lines:Vec<String> = Vec::with_capacity(root.nexts.len());
    for n in &root.nexts {
        debug_lines.push(format!("[{}] Q={:.3} W={:.3} N={}",
            n.loc, n.avg_wins(), n.avg_whole(), n.n_select
        ));
    }
    fns.debug(debug_lines.join("\n"));
    root.nexts[0].loc
}

fn search_tree<WF:Fn(&Board,Piece)->f32>(pcur:Piece, board:&Board, depth:usize, mut alpha:f32, beta:f32, wf:&WF, count:&mut usize) -> f32 {
    if depth > 0 {
        let moves = board.moves(pcur);
        if moves.len() > 0 {
            let mut node_board = board.clone();
            for loc in moves {
                node_board.down(loc, pcur);
                let value = -search_tree(pcur.side(), &node_board, depth-1, -beta, -alpha, wf, count);
                alpha = alpha.max(value);
                if alpha >= beta {break}
                node_board.load(board);
            }
            alpha
        } else {
            -search_tree(pcur.side(), board, depth-1, -beta, -alpha, wf, count)
        }
    } else {
        *count += 1;
        wf(board, pcur)
    }
}
fn step_search_tree<WF:Fn(&Board,Piece)->f32>(pcur:Piece, board:&Board, locs:&[Loc], level:usize, wf:&WF, fns:&dyn YieldFns) -> (f32,Loc,usize) {
    let depth_max = LEVEL_SEARCH_MAP[level];
    let mut count = 0;
    let mut hit_value = 1.0;
    let mut hit_loc = locs[0];
    for loc in locs {
        let mut node_board = board.clone();
        node_board.down(*loc, pcur);
        fns.report(format!("Search {:.1}W", (count as f32) / 10000.0));
        let value = search_tree(pcur.side(), &node_board, depth_max, f32::MIN, f32::MAX, wf, &mut count);
        if value < hit_value {
            hit_value = value;
            hit_loc = *loc;
        }
    }
    let win_rate = 0.5 - (hit_value / 2.0);
    (win_rate, hit_loc, count)
}

fn full_tree(pcur:Piece, board:&Board, depth:usize, mut alpha:f32, mut beta:f32, count:&mut usize) -> f32 {
    if depth > 0 {
        let moves = SHARED.moves(board, pcur);
        if moves.len() > 0 {
            let mut node_board = board.clone();
            if pcur == Piece::White {
                for loc in moves {
                    node_board.down(loc, pcur);
                    let node_value = full_tree(pcur.side(), &node_board, depth-1, alpha, beta, count);
                    if node_value > alpha {
                        alpha = node_value
                    }
                    if alpha >= beta {break}
                    node_board.load(board);
                }
                alpha
            } else {
                for loc in moves{
                    node_board.down(loc, pcur);
                    let node_value = full_tree(pcur.side(), &node_board, depth-1, alpha, beta, count);
                    if node_value < beta {
                        beta = node_value
                    }
                    if alpha >= beta {break}
                    node_board.load(board);
                }
                beta
            }
        } else {
            full_tree(pcur.side(), board, depth-1,  alpha, beta, count)
        }
    } else {
        *count += 1;
        valuation_n_pieces(board.n_black(), board.n_white())
    }
}
fn step_full_tree(pcur:Piece, board:&Board, locs:&[Loc], depth_max:usize, fns:&dyn YieldFns) -> (f32,Loc,usize) {
    let mut hit = (-pcur.way(), locs[0], 0);
    let mut locs:Vec<Loc> = locs.into();
    while ! locs.is_empty() {
        let li = rand::random_range(0..locs.len());
        let loc = locs.swap_remove(li);
        let mut node_board = board.clone();
        node_board.down(loc, pcur);
        fns.report(format!("In {}", loc));
        let value = full_tree(pcur.side(), &node_board, depth_max, -1.0, 1.0, &mut hit.2);
        if pcur == Piece::Black && value < hit.0 {
            hit.0 = value; hit.1 = loc;
            if value <= -1.0 {break}
        } else if pcur == Piece::White && value > hit.0 {
            hit.0 = value; hit.1 = loc;
            if value >= 1.0 {break}
        }
    }
    hit
}

fn beging(pcur:Piece, board:&Board, locs:&[Loc], level:usize, fns:&dyn YieldFns) -> Loc {
    let mut scores:Vec<(Loc,f32)> = locs.iter().map(|&loc| {
        let mut board_next = board.clone();
        board_next.down(loc, pcur);
        let a = valuation_being(&board_next) * pcur.way();
        (loc,a)
    }).collect();
    scores.sort_by(|a,b|{b.1.total_cmp(&a.1)});
    let mut debug_lines:Vec<String> = Vec::with_capacity(scores.len());
    for n in &scores {
        debug_lines.push(format!("[{}] S={:.3}", n.0, n.1));
    }
    fns.debug(debug_lines.join("\n"));
    scores[0].0
}

pub fn step(pcur:Piece, board:&Board, uct_times:usize, level:usize, fns:&dyn YieldFns) -> Option<Loc> {
    let init_moves = SHARED.moves(board, pcur);
    if init_moves.len() > 1 {
        let _ = SHARED.last_board.set(0, board.clone());
        let n_piece = board.n_piece();
        if n_piece < N_PIECES_IN_MID {
            let has_in_border = init_moves.iter().any(|loc| {LOCS_BEGIN_RANGOM[*loc as usize] != 1});
            if ! has_in_border {
                fns.debug(format!("sslash-local | random | init-moves {:?}", init_moves));
                return Some(beging(pcur, board, &init_moves, level, fns));
            }
        }
        let lws = SHARED.lws();
        let lw_sums = [lws[0].iter().sum::<f32>(), lws[1].iter().sum::<f32>()];
        let wf = |board:&Board,pcur:Piece| -> f32 {
            let pi = pcur as usize - 1;
            valuation_whole(board, pcur, lws[pi], lw_sums[pi])
        };
        if n_piece < N_PIECES_IN_SEARCH {
            fns.debug(format!("sslash-local | uct | init-moves {:?}", init_moves));
            Some(step_uct(pcur, board, uct_times, level, &wf, fns))
        } else if n_piece < N_PIECES_IN_ENDING - (level / 2) {
            let (win_rate, loc, count) = step_search_tree(pcur, board, &init_moves, level, &wf, fns);
            fns.debug(format!("sslash | search L{} | [{}] W={:.3} N={}", level, loc, win_rate, count));
            fns.say(format!("{:.0}%", win_rate * 100.0));
            Some(loc)
        } else {
            fns.debug(format!("sslash-local | tree | init-moves {:?}", init_moves));
            let depth_max = BOARD_LEN - n_piece;
            let (value,loc, count) = step_full_tree(pcur, board, &init_moves, depth_max, fns);
            fns.debug(format!("[{loc}] V={value} N={count}"));
            if ((pcur == Piece::Black) && (value <= -1.0)) || ((pcur == Piece::White) && (value >= 1.0)) {
                fns.say(SAY_HAS_WIN.into())
            } else if ((pcur == Piece::Black) && (value <= 0.0)) || ((pcur == Piece::White) && (value >= 0.0)) {
                fns.say(SAY_MAYBE_DRAW.into())
            } else if ((pcur == Piece::Black) && (value >= 1.0)) || ((pcur == Piece::White) && (value <= 1.0)) {
                fns.say(SAY_WILL_LOSE.into())
            } else {
                fns.say(SAY_WHAT.into())
            }
            Some(loc)
        }
    } else if init_moves.len() == 1 {
        Some(init_moves[0])
    } else {
        None
    }
}

fn make_random_game(init_board:Board) -> Board {
    let mut board = init_board;
    let board_n_piece = board.n_piece();
    let n_piece = rand::random_range(board_n_piece..board_n_piece+10);
    let mut n_piece =  n_piece.clamp(board_n_piece, BOARD_LEN) - board_n_piece;
    let mut n_pass = 0;
    let mut side = Piece::Black;
    while (n_piece > 0) && (n_pass < 1) {
        let moves = moves_cached(&board, side);
        if moves.is_empty() {
            n_pass += 1
        } else {
            let loc = moves[rand::random_range(0..moves.len())];
            board.down(loc, side);
            n_piece -= 1;
            n_pass = 0;
        }
        side = side.side();
    }
    board
}
fn background_moves_gen() {
    let board = if let Some(last_board) = SHARED.last_board.get_ref(0) {
        last_board.clone()
    } else {
        Board::make_random_board_by_init(0, BOARD_LEN)
    };
    make_random_game(board);
}
fn background_main(cfg:config::Engine) {
    log::info!("[engine-ai] background start");
    let mut whole_leran:WholeFrame = match WholeFrame::load(&cfg.whole_bin_name) {
        Ok(a) => {
            log::info!("[engine-ai] load whole-bin '{}' | {} | n={} nw-black={} nw-white={}", cfg.whole_bin_name,
                a.status(), a.times(), a.nw_black(), a.nw_white()
            );
            let _ = SHARED.whole_locs.set(0, a.table_black());
            let _ = SHARED.whole_locs.set(1, a.table_white());
            a
        }
        Err(_) => {
            log::info!("[engine-ai] make whole-bin '{}'", cfg.whole_bin_name);
            WholeFrame::new()
        }
    };
    let mut enable_whole_learn = cfg.allow_whole_learn && (whole_leran.status().learn_able());
    if enable_whole_learn {
        log::info!("[engine-ai] enable whole learn");
    }
    let whole_times_before = whole_leran.line_count();
    while SHARED.background_running.load(Ordering::Relaxed) {
        if cfg.allow_bg_moves_gen {
            background_moves_gen();
        }
        if enable_whole_learn && whole_leran.line_count() < LERAN_WHOLE_MAX_TIMES {
            whole_leran.frame();
        } else {
            enable_whole_learn = false;
        }
    }
    if whole_leran.line_count() > whole_times_before {
        match whole_leran.save(&cfg.whole_bin_name) {
            Ok(()) => {
                log::info!("[engine-ai] whole-bin '{}' has updated(add {})", cfg.whole_bin_name, whole_leran.line_count() - whole_times_before);
            }
            Err(e) => {
                log::warning!("[engine-ai] whole-bin '{}' update failure: {}", cfg.whole_bin_name, e);
            }
        }
    }
    log::info!("[engine-ai] background end");
}

pub fn _init(config:&Config) -> Result<()> {
    let default_whole = WholeFrame::default();
    let _ = SHARED.whole_locs.set(0, default_whole.table_black());
    let _ = SHARED.whole_locs.set(1, default_whole.table_white());
    let cfg = &config.engine;
    if cfg.allow_background {
        SHARED.background_running.store(true, Ordering::Relaxed);
        let bg_config = config.engine.clone();
        let handle = thread::spawn(|| {background_main(bg_config)});
        let _ = SHARED.background_handle.set(0, handle);
    }
    let moves_fn = if cfg.use_moves_cache {
        assert!(SHARED.moves_cache_table.set(MovesCacheTable::new()).is_ok());
        log::info!("[engine-ai] initialize moves cache table(len:{})", SHARED.moves_cache_table().capacity());
        log::info!("[engine-ai] use moves cache");
        moves_cached
    } else {
        moves_normal
    };
    assert!(SHARED.moves_fn.set(Box::new(moves_fn)).is_ok());
    log::info!("[engine-ai] initialized");
    Ok(())
}
pub fn _raii() {
    if let Some(handle) = SHARED.background_handle.remove(0) {
        SHARED.background_running.store(false, Ordering::Relaxed);
        if let Err(_) = handle.join() {
            log::warning!("[engine-ai] background thread incorrect fuck");
        }
    }
    log::info!("[engine-ai] raii");
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::time::Duration;
    use crate::utils::pftest;

    fn with_test<F:Fn()->Result<()>>(f:F) -> Result<()> {
        log::_init_for_test()?;
        _init(&Config::default())?;
        let r = f();
        _raii();
        r
    }

    #[test]
    fn ai_moves_cache() -> Result<()> {
        with_test(|| {
            log::test!("test: ai - moves cache");
            for _ in 0..65536 {
                let board = Board::make_random_board_by_init(4, BOARD_LEN);
                let moves_no_cache_black = board.moves(Piece::Black);
                let moves_no_cache_white = board.moves(Piece::White);
                assert_eq!(moves_no_cache_black, moves_cached(&board, Piece::Black));
                assert_eq!(moves_no_cache_white, moves_cached(&board, Piece::White));
            }
            Ok(())
        })
    }

    #[test]
    fn pf_ai_moves_cache() -> Result<()> {
        with_test(||{
            log::test!("pf: ai - moves cache");
            let board = Board::make_random_board_by_init(10, BOARD_LEN);
            let max_time = Duration::from_secs(1);
            {
                log::test!("pf-board-io per-1s moves-cached(high-hit):\n{:?}", pftest::per_time(move ||{
                    #[allow(unused)]
                    let blacks = moves_cached(&board, Piece::Black);
                    #[allow(unused)]
                    let whites = moves_cached(&board, Piece::White);
                }, max_time));
            }
            {
                let init_board = Board::make_random_board_by_init(0, 10);
                let _ = SHARED.last_board.set(0, init_board);
                log::test!("pf-board-io per-1s moves-cached(normal-hit):\n{:?}", pftest::per_time(move || {
                    let init_board = SHARED.last_board.get_ref(0).unwrap().clone();
                    let board = Board::make_random_board(init_board, 0, init_board.n_piece()+3);
                    #[allow(unused)]
                    let blacks = moves_cached(&board, Piece::Black);
                    #[allow(unused)]
                    let whites = moves_cached(&board, Piece::White);
                }, max_time));
            }
            Ok(())
        })
    }
}
